import {
  CommunicationWay,
  ServiceHandler,
  ServiceRegister,
} from "@main/shared/reflect";
import { TYPES } from "@main/shared/types";
import logger from "@shared/logger/main-logger";
import { restoreRenderMap } from "@shared/prompt-editor/utils";
import type {
  CreateMessageData,
  Message,
  SendUserMessageParams,
  UpdateMessageData,
} from "@shared/triplit/types";
import { inject, injectable } from "inversify";
import type { MessageDbService } from "./db-service/message-db-service";
import { EventNames, emitter, sendToMain, sendToThread } from "./event-service";
import type { ChatMessage } from "./provider-service/base-provider-service";

@injectable()
@ServiceRegister(TYPES.MessageService)
export class MessageService {
  constructor(
    @inject(TYPES.MessageDbService) private messageDbService: MessageDbService,
  ) {
    this.updatePendingMessagesToStop();
    emitter.on(EventNames.THREAD_NEW_BRANCH, async (event) => {
      await this.handleThreadNewBranch(event);
    });

    emitter.on(EventNames.MESSAGE_RENDERED, async (event) => {
      const { messageId, template, renderedContent, variables, renderMap } =
        event;
      await this.handleMessageRendered(
        messageId,
        template,
        renderedContent,
        variables,
        renderMap,
      );
    });
  }

  private async handleThreadNewBranch(event: {
    originalThreadId: string;
    messageId: string;
    newThreadId: string;
  }): Promise<void> {
    try {
      const messages = await this._getMessagesByThreadId(
        event.originalThreadId,
      );
      const currentMessageIndex = messages.findIndex(
        (msg) => msg.id === event.messageId,
      );

      const messagesToInsert = messages.slice(0, currentMessageIndex + 1);
      const messagesToInsertData: CreateMessageData[] = messagesToInsert.map(
        (msg) => {
          const { id: _id, ...msgWithoutIdAndDate } = msg;
          return { ...msgWithoutIdAndDate, threadId: event.newThreadId };
        },
      );

      await this._insertMessages(messagesToInsertData);
    } catch (error) {
      logger.error("MessageService: _insertMessages error", { error });
      throw error;
    }

    emitter.on(
      EventNames.MESSAGE_RENDERED,
      async ({
        messageId,
        template,
        renderedContent,
        variables,
        renderMap,
      }) => {
        await this.handleMessageRendered(
          messageId,
          template,
          renderedContent,
          variables,
          renderMap,
        );
      },
    );
  }

  private async handleMessageRendered(
    messageId: string,
    template: string,
    renderedContent: string,
    variables: string[],
    renderMap: Record<string, string>,
  ): Promise<void> {
    try {
      await this.messageDbService.updateMessage(messageId, {
        userPromptTemplate: template,
        variables: new Set(variables),
        renderMap: JSON.stringify(renderMap),
        renderedContent,
      });
    } catch (error) {
      logger.error("MessageService: updateUserPromptTemplate error", {
        error,
      });
      throw error;
    }
  }

  async _getMessagesByThreadId(threadId: string): Promise<Message[]> {
    const messages =
      await this.messageDbService.getMessagesByThreadId(threadId);
    return messages;
  }

  async _insertMessage(message: CreateMessageData): Promise<Message> {
    return await this.messageDbService.insertMessage(message);
  }

  async _insertMessages(messages: CreateMessageData[]): Promise<void> {
    await this.messageDbService.insertMessages(messages);
  }

  async _updateMessage(
    messageId: string,
    updateData: UpdateMessageData,
  ): Promise<void> {
    await this.messageDbService.updateMessage(messageId, updateData);
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__TWO_WAY)
  async sendUserMessage(
    _event: Electron.IpcMainEvent,
    message: SendUserMessageParams,
  ): Promise<Message> {
    const { selectedMcpServerIds, ...messageData } = message;
    const userMessage: CreateMessageData = {
      ...messageData,
      role: "user",
      tokenCount: 0,
      status: "success",
      isThinkBlockCollapsed: false,
      hasSequentialContent: false,
      parentMessageId: null,
    };
    const newMessage = await this.messageDbService.insertMessage(userMessage);
    sendToMain(EventNames.MESSAGE_SEND_FROM_USER, {
      threadId: message.threadId,
      selectedMcpServerIds: selectedMcpServerIds || [],
    });
    return newMessage;
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async updateMessage(
    _event: Electron.IpcMainEvent,
    messageId: string,
    updateData: UpdateMessageData,
  ): Promise<void> {
    try {
      await this.messageDbService.updateMessage(messageId, updateData);
    } catch (error) {
      logger.error("MessageService: updateMessage error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async editMessage(
    _event: Electron.IpcMainEvent,
    messageId: string,
    editData: {
      threadId: string;
    } & Pick<Message, "content">,
  ): Promise<void> {
    try {
      const updatedMessage = await this.messageDbService.updateMessage(
        messageId,
        editData,
      );

      sendToMain(EventNames.MESSAGE_ACTIONS, {
        threadId: editData.threadId,
        actions: {
          type: "edit",
        },
      });
      sendToThread(editData.threadId, EventNames.MESSAGE_ACTIONS, {
        threadId: editData.threadId,
        actions: {
          type: "edit",
          message: updatedMessage,
        },
      });
    } catch (error) {
      logger.error("MessageService: editMessage error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async deleteMessage(
    _event: Electron.IpcMainEvent,
    messageId: string,
    threadId: string,
  ): Promise<void> {
    try {
      // Get message before deleting for event notification
      const messageToDelete =
        await this.messageDbService.getMessageById(messageId);

      await this.messageDbService.deleteMessage(messageId);

      if (messageToDelete) {
        sendToMain(EventNames.MESSAGE_ACTIONS, {
          threadId,
          actions: {
            type: "delete-single",
          },
        });
        sendToThread(threadId, EventNames.MESSAGE_ACTIONS, {
          threadId,
          actions: {
            type: "delete-single",
            message: messageToDelete,
          },
        });
      }
    } catch (error) {
      logger.error("MessageService: deleteMessage error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__TWO_WAY)
  async getMessagesByThreadId(
    _event: Electron.IpcMainEvent,
    threadId: string,
  ): Promise<Message[]> {
    try {
      const messages =
        await this.messageDbService.getMessagesByThreadId(threadId);
      return messages;
    } catch (error) {
      logger.error("MessageService: getMessagesByThreadId error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__TWO_WAY)
  async getMessageById(
    _event: Electron.IpcMainEvent,
    messageId: string,
  ): Promise<Message | null> {
    try {
      const message = await this.messageDbService.getMessageById(messageId);
      return message;
    } catch (error) {
      logger.error("MessageService: getMessageById error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async deleteMessagesByThreadId(
    _event: Electron.IpcMainEvent,
    threadId: string,
  ): Promise<void> {
    try {
      await this.messageDbService.deleteMessagesByThreadId(threadId);
      sendToMain(EventNames.MESSAGE_ACTIONS, {
        threadId,
        actions: {
          type: "delete",
        },
      });
      sendToThread(threadId, EventNames.MESSAGE_ACTIONS, {
        threadId,
        actions: {
          type: "delete",
        },
      });
    } catch (error) {
      logger.error("MessageService: deleteMessagesByThreadId error", {
        error,
      });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async deleteAllMessages(
    _event: Electron.IpcMainEvent,
    threadIds?: string[],
  ): Promise<void> {
    try {
      if (threadIds) {
        await Promise.all(
          threadIds.map((threadId) =>
            this.messageDbService.deleteMessagesByThreadId(threadId),
          ),
        );
      } else {
        await this.messageDbService.deleteAllMessages();
      }
    } catch (error) {
      logger.error("MessageService: deleteAllMessages error", { error });
      throw error;
    }
  }

  @ServiceHandler(CommunicationWay.RENDERER_TO_MAIN__ONE_WAY)
  async insertMessages(
    _event: Electron.IpcMainEvent,
    messages: CreateMessageData[],
  ): Promise<void> {
    try {
      await this.messageDbService.insertMessages(messages);
    } catch (error) {
      logger.error("MessageService: insertMessages error", { error });
      throw error;
    }
  }

  /**
   * Updates any messages with pending status to "stop" on service initialization.
   *
   * This cleanup prevents messages from being stuck in pending state after
   * application restarts or crashes, ensuring UI consistency.
   */
  async updatePendingMessagesToStop(): Promise<void> {
    try {
      await this.messageDbService.updatePendingMessagesToStop();
    } catch (error) {
      logger.error("MessageService: updatePendingMessagesToStop error", {
        error,
      });
      throw error;
    }
  }

  async renderMessagesByTemplate(
    messages: ChatMessage[],
  ): Promise<ChatMessage[]> {
    try {
      logger.info("renderMessagesByTemplate", {
        messages,
      });

      const renderedMessages = await Promise.all(
        messages.map(async (message) => {
          if (!message.id || message.role !== "user") return message;
          const dbMessage = await this.messageDbService.getMessageById(
            message.id,
          );
          if (!dbMessage) return message;

          const { userPromptTemplate, variables, renderMap } = dbMessage;
          if (!userPromptTemplate) return message;
          if (!variables || !variables.has("now")) return message;
          if (!renderMap) return message;

          const renderedContent = restoreRenderMap(renderMap);
          if (!renderedContent) return message;

          await this.messageDbService.updateMessage(message.id, {
            renderedContent,
          });

          return {
            ...message,
            renderedContent,
          };
        }),
      );

      return renderedMessages;
    } catch (error) {
      logger.error("MessageService: renderMessagesByTemplate error", {
        error,
      });
      throw error;
    }
  }
}
